#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright © 2018, 2019 Endless Mobile, Inc.
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA  02110-1301  USA

"""Integration tests for gdbus-codegen utility."""

import collections
import os
import shutil
import subprocess
import sys
import tempfile
import textwrap
import unittest
import xml.etree.ElementTree as ET

import taptestrunner

# Disable line length warnings as wrapping the C code templates would be hard
# flake8: noqa: E501


Result = collections.namedtuple("Result", ("info", "out", "err", "subs"))


def on_win32():
    return sys.platform.find("win") != -1


class TestCodegen(unittest.TestCase):
    """Integration test for running gdbus-codegen.

    This can be run when installed or uninstalled. When uninstalled, it
    requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set.

    The idea with this test harness is to test the gdbus-codegen utility, its
    handling of command line arguments, its exit statuses, and its handling of
    various C source codes. In future we could split out tests for the core
    parsing and generation code of gdbus-codegen into separate unit tests, and
    just test command line behaviour in this integration test.
    """

    # Track the cwd, we want to back out to that to clean up our tempdir
    cwd = ""

    ARGUMENTS_TYPES = {
        "b": {"value_type": "boolean"},
        "y": {"value_type": "uchar"},
        "n": {"value_type": "int"},
        "q": {"value_type": "uint"},
        "i": {"value_type": "int"},
        "u": {"value_type": "uint"},
        "x": {"value_type": "int64", "lacks_marshaller": True},
        "t": {"value_type": "uint64", "lacks_marshaller": True},
        "d": {"value_type": "double"},
        "s": {"value_type": "string"},
        "o": {"value_type": "string"},
        "g": {"value_type": "string"},
        "h": {"value_type": "variant"},
        "ay": {"value_type": "string"},
        "as": {"value_type": "boxed"},
        "ao": {"value_type": "boxed"},
        "aay": {"value_type": "boxed"},
        "asv": {"value_type": "variant", "variant_type": "a{sv}"},
    }

    def setUp(self):
        self.timeout_seconds = 6  # seconds per test
        self.tmpdir = tempfile.TemporaryDirectory()
        self.cwd = os.getcwd()
        os.chdir(self.tmpdir.name)
        print("tmpdir:", self.tmpdir.name)
        if "G_TEST_BUILDDIR" in os.environ:
            self.__codegen = os.path.join(
                os.environ["G_TEST_BUILDDIR"],
                "..",
                "gdbus-2.0",
                "codegen",
                "gdbus-codegen",
            )
        else:
            self.__codegen = shutil.which("gdbus-codegen")
        print("codegen:", self.__codegen)

    def tearDown(self):
        os.chdir(self.cwd)
        self.tmpdir.cleanup()

    def runCodegen(self, *args):
        argv = [self.__codegen]

        # shebang lines are not supported on native
        # Windows consoles
        if os.name == "nt":
            argv.insert(0, sys.executable)

        argv.extend(args)
        print("Running:", argv)

        env = os.environ.copy()
        env["LC_ALL"] = "C.UTF-8"
        env["G_DEBUG"] = "fatal-warnings"
        print("Environment:", env)

        # We want to ensure consistent line endings...
        info = subprocess.run(
            argv,
            timeout=self.timeout_seconds,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=env,
            universal_newlines=True,
        )
        info.check_returncode()
        out = info.stdout.strip()
        err = info.stderr.strip()

        # Known substitutions for standard boilerplate
        subs = {
            "standard_top_comment": "/*\n"
            " * This file is generated by gdbus-codegen, do not modify it.\n"
            " *\n"
            " * The license of this code is the same as for the D-Bus interface description\n"
            " * it was derived from. Note that it links to GLib, so must comply with the\n"
            " * LGPL linking clauses.\n"
            " */",
            "standard_config_h_include": "#ifdef HAVE_CONFIG_H\n"
            '#  include "config.h"\n'
            "#endif",
            "standard_header_includes": "#include <string.h>\n"
            "#ifdef G_OS_UNIX\n"
            "#  include <gio/gunixfdlist.h>\n"
            "#endif",
            "private_gvalues_getters": """#ifdef G_ENABLE_DEBUG
#define g_marshal_value_peek_boolean(v)  g_value_get_boolean (v)
#define g_marshal_value_peek_char(v)     g_value_get_schar (v)
#define g_marshal_value_peek_uchar(v)    g_value_get_uchar (v)
#define g_marshal_value_peek_int(v)      g_value_get_int (v)
#define g_marshal_value_peek_uint(v)     g_value_get_uint (v)
#define g_marshal_value_peek_long(v)     g_value_get_long (v)
#define g_marshal_value_peek_ulong(v)    g_value_get_ulong (v)
#define g_marshal_value_peek_int64(v)    g_value_get_int64 (v)
#define g_marshal_value_peek_uint64(v)   g_value_get_uint64 (v)
#define g_marshal_value_peek_enum(v)     g_value_get_enum (v)
#define g_marshal_value_peek_flags(v)    g_value_get_flags (v)
#define g_marshal_value_peek_float(v)    g_value_get_float (v)
#define g_marshal_value_peek_double(v)   g_value_get_double (v)
#define g_marshal_value_peek_string(v)   (char*) g_value_get_string (v)
#define g_marshal_value_peek_param(v)    g_value_get_param (v)
#define g_marshal_value_peek_boxed(v)    g_value_get_boxed (v)
#define g_marshal_value_peek_pointer(v)  g_value_get_pointer (v)
#define g_marshal_value_peek_object(v)   g_value_get_object (v)
#define g_marshal_value_peek_variant(v)  g_value_get_variant (v)
#else /* !G_ENABLE_DEBUG */
/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API.
 *          Do not access GValues directly in your code. Instead, use the
 *          g_value_get_*() functions
 */
#define g_marshal_value_peek_boolean(v)  (v)->data[0].v_int
#define g_marshal_value_peek_char(v)     (v)->data[0].v_int
#define g_marshal_value_peek_uchar(v)    (v)->data[0].v_uint
#define g_marshal_value_peek_int(v)      (v)->data[0].v_int
#define g_marshal_value_peek_uint(v)     (v)->data[0].v_uint
#define g_marshal_value_peek_long(v)     (v)->data[0].v_long
#define g_marshal_value_peek_ulong(v)    (v)->data[0].v_ulong
#define g_marshal_value_peek_int64(v)    (v)->data[0].v_int64
#define g_marshal_value_peek_uint64(v)   (v)->data[0].v_uint64
#define g_marshal_value_peek_enum(v)     (v)->data[0].v_long
#define g_marshal_value_peek_flags(v)    (v)->data[0].v_ulong
#define g_marshal_value_peek_float(v)    (v)->data[0].v_float
#define g_marshal_value_peek_double(v)   (v)->data[0].v_double
#define g_marshal_value_peek_string(v)   (v)->data[0].v_pointer
#define g_marshal_value_peek_param(v)    (v)->data[0].v_pointer
#define g_marshal_value_peek_boxed(v)    (v)->data[0].v_pointer
#define g_marshal_value_peek_pointer(v)  (v)->data[0].v_pointer
#define g_marshal_value_peek_object(v)   (v)->data[0].v_pointer
#define g_marshal_value_peek_variant(v)  (v)->data[0].v_pointer
#endif /* !G_ENABLE_DEBUG */""",
            "standard_typedefs_and_helpers": "typedef struct\n"
            "{\n"
            "  GDBusArgInfo parent_struct;\n"
            "  gboolean use_gvariant;\n"
            "} _ExtendedGDBusArgInfo;\n"
            "\n"
            "typedef struct\n"
            "{\n"
            "  GDBusMethodInfo parent_struct;\n"
            "  const gchar *signal_name;\n"
            "  gboolean pass_fdlist;\n"
            "} _ExtendedGDBusMethodInfo;\n"
            "\n"
            "typedef struct\n"
            "{\n"
            "  GDBusSignalInfo parent_struct;\n"
            "  const gchar *signal_name;\n"
            "} _ExtendedGDBusSignalInfo;\n"
            "\n"
            "typedef struct\n"
            "{\n"
            "  GDBusPropertyInfo parent_struct;\n"
            "  const gchar *hyphen_name;\n"
            "  guint use_gvariant : 1;\n"
            "  guint emits_changed_signal : 1;\n"
            "} _ExtendedGDBusPropertyInfo;\n"
            "\n"
            "typedef struct\n"
            "{\n"
            "  GDBusInterfaceInfo parent_struct;\n"
            "  const gchar *hyphen_name;\n"
            "} _ExtendedGDBusInterfaceInfo;\n"
            "\n"
            "typedef struct\n"
            "{\n"
            "  const _ExtendedGDBusPropertyInfo *info;\n"
            "  guint prop_id;\n"
            "  GValue orig_value; /* the value before the change */\n"
            "} ChangedProperty;\n"
            "\n"
            "static void\n"
            "_changed_property_free (ChangedProperty *data)\n"
            "{\n"
            "  g_value_unset (&data->orig_value);\n"
            "  g_free (data);\n"
            "}\n"
            "\n"
            "static gboolean\n"
            "_g_strv_equal0 (gchar **a, gchar **b)\n"
            "{\n"
            "  gboolean ret = FALSE;\n"
            "  guint n;\n"
            "  if (a == NULL && b == NULL)\n"
            "    {\n"
            "      ret = TRUE;\n"
            "      goto out;\n"
            "    }\n"
            "  if (a == NULL || b == NULL)\n"
            "    goto out;\n"
            "  if (g_strv_length (a) != g_strv_length (b))\n"
            "    goto out;\n"
            "  for (n = 0; a[n] != NULL; n++)\n"
            "    if (g_strcmp0 (a[n], b[n]) != 0)\n"
            "      goto out;\n"
            "  ret = TRUE;\n"
            "out:\n"
            "  return ret;\n"
            "}\n"
            "\n"
            "static gboolean\n"
            "_g_variant_equal0 (GVariant *a, GVariant *b)\n"
            "{\n"
            "  gboolean ret = FALSE;\n"
            "  if (a == NULL && b == NULL)\n"
            "    {\n"
            "      ret = TRUE;\n"
            "      goto out;\n"
            "    }\n"
            "  if (a == NULL || b == NULL)\n"
            "    goto out;\n"
            "  ret = g_variant_equal (a, b);\n"
            "out:\n"
            "  return ret;\n"
            "}\n"
            "\n"
            "G_GNUC_UNUSED static gboolean\n"
            "_g_value_equal (const GValue *a, const GValue *b)\n"
            "{\n"
            "  gboolean ret = FALSE;\n"
            "  g_assert (G_VALUE_TYPE (a) == G_VALUE_TYPE (b));\n"
            "  switch (G_VALUE_TYPE (a))\n"
            "    {\n"
            "      case G_TYPE_BOOLEAN:\n"
            "        ret = (g_value_get_boolean (a) == g_value_get_boolean (b));\n"
            "        break;\n"
            "      case G_TYPE_UCHAR:\n"
            "        ret = (g_value_get_uchar (a) == g_value_get_uchar (b));\n"
            "        break;\n"
            "      case G_TYPE_INT:\n"
            "        ret = (g_value_get_int (a) == g_value_get_int (b));\n"
            "        break;\n"
            "      case G_TYPE_UINT:\n"
            "        ret = (g_value_get_uint (a) == g_value_get_uint (b));\n"
            "        break;\n"
            "      case G_TYPE_INT64:\n"
            "        ret = (g_value_get_int64 (a) == g_value_get_int64 (b));\n"
            "        break;\n"
            "      case G_TYPE_UINT64:\n"
            "        ret = (g_value_get_uint64 (a) == g_value_get_uint64 (b));\n"
            "        break;\n"
            "      case G_TYPE_DOUBLE:\n"
            "        {\n"
            "          /* Avoid -Wfloat-equal warnings by doing a direct bit compare */\n"
            "          gdouble da = g_value_get_double (a);\n"
            "          gdouble db = g_value_get_double (b);\n"
            "          ret = memcmp (&da, &db, sizeof (gdouble)) == 0;\n"
            "        }\n"
            "        break;\n"
            "      case G_TYPE_STRING:\n"
            "        ret = (g_strcmp0 (g_value_get_string (a), g_value_get_string (b)) == 0);\n"
            "        break;\n"
            "      case G_TYPE_VARIANT:\n"
            "        ret = _g_variant_equal0 (g_value_get_variant (a), g_value_get_variant (b));\n"
            "        break;\n"
            "      default:\n"
            "        if (G_VALUE_TYPE (a) == G_TYPE_STRV)\n"
            "          ret = _g_strv_equal0 (g_value_get_boxed (a), g_value_get_boxed (b));\n"
            "        else\n"
            '          g_critical ("_g_value_equal() does not handle type %s", g_type_name (G_VALUE_TYPE (a)));\n'
            "        break;\n"
            "    }\n"
            "  return ret;\n"
            "}",
        }

        result = Result(info, out, err, subs)

        print("Output:", result.out)
        return result

    def runCodegenWithInterface(self, interface_contents, *args):
        with tempfile.NamedTemporaryFile(
            dir=self.tmpdir.name, suffix=".xml", delete=False
        ) as interface_file:
            # Write out the interface.
            interface_file.write(interface_contents.encode("utf-8"))
            print(interface_file.name + ":", interface_contents)
            interface_file.flush()

            return self.runCodegen(interface_file.name, *args)

    def test_help(self):
        """Test the --help argument."""
        result = self.runCodegen("--help")
        self.assertIn("usage: gdbus-codegen", result.out)

    def test_no_args(self):
        """Test running with no arguments at all."""
        with self.assertRaises(subprocess.CalledProcessError):
            self.runCodegen()

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_empty_interface_header(self):
        """Test generating a header with an empty interface file."""
        result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--header")
        self.assertEqual("", result.err)
        self.assertEqual(
            """{standard_top_comment}

#ifndef __STDOUT__
#define __STDOUT__

#include <gio/gio.h>

G_BEGIN_DECLS


G_END_DECLS

#endif /* __STDOUT__ */""".format(
                **result.subs
            ),
            result.out.strip(),
        )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_empty_interface_body(self):
        """Test generating a body with an empty interface file."""
        result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--body")
        self.assertEqual("", result.err)
        self.assertEqual(
            """{standard_top_comment}

{standard_config_h_include}

#include "stdout.h"

{standard_header_includes}

{private_gvalues_getters}

{standard_typedefs_and_helpers}""".format(
                **result.subs
            ),
            result.out.strip(),
        )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_reproducible(self):
        """Test builds are reproducible regardless of file ordering."""
        xml_contents1 = """
        <node>
          <interface name="com.acme.Coyote">
            <method name="Run"/>
            <method name="Sleep"/>
            <method name="Attack"/>
            <signal name="Surprised"/>
            <property name="Mood" type="s" access="read"/>
          </interface>
        </node>
        """

        xml_contents2 = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <method name="RandomMethod"/>
          </interface>
        </node>
        """

        with tempfile.NamedTemporaryFile(
            dir=self.tmpdir.name, suffix="1.xml", delete=False
        ) as xml_file1, tempfile.NamedTemporaryFile(
            dir=self.tmpdir.name, suffix="2.xml", delete=False
        ) as xml_file2:
            # Write out the interfaces.
            xml_file1.write(xml_contents1.encode("utf-8"))
            xml_file2.write(xml_contents2.encode("utf-8"))

            xml_file1.flush()
            xml_file2.flush()

            # Repeat this for headers and bodies.
            for header_or_body in ["--header", "--body"]:
                # Run gdbus-codegen with the interfaces in one order, and then
                # again in another order.
                result1 = self.runCodegen(
                    xml_file1.name,
                    xml_file2.name,
                    "--output",
                    "/dev/stdout",
                    header_or_body,
                )
                self.assertEqual("", result1.err)

                result2 = self.runCodegen(
                    xml_file2.name,
                    xml_file1.name,
                    "--output",
                    "/dev/stdout",
                    header_or_body,
                )
                self.assertEqual("", result2.err)

                # The output should be the same.
                self.assertEqual(result1.out, result2.out)

    def test_generate_docbook(self):
        """Test the basic functionality of the docbook generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <method name="RandomMethod"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-docbook",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.xml", "r") as f:
            xml_data = f.readlines()
            self.assertTrue(len(xml_data) != 0)

    def test_generate_md(self):
        """Test the basic functionality of the markdown generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <method name="RandomMethod"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-md",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.md", "r") as f:
            rst = f.readlines()
            self.assertTrue(len(rst) != 0)

    def test_generate_rst(self):
        """Test the basic functionality of the rst generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <method name="RandomMethod"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-rst",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.rst", "r") as f:
            rst = f.readlines()
            self.assertTrue(len(rst) != 0)

    def test_generate_rst_method(self):
        """Test generating a method documentation with the rst generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <!-- RandomMethod:

            A random test method.
            -->
            <method name="RandomMethod"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-rst",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.rst", "r") as f:
            rst = f.read()
            self.assertIn(
                textwrap.dedent(
                    """
                    -------
                    Methods
                    -------

                    .. _org.project.Bar.Frobnicator.RandomMethod:

                    org.project.Bar.Frobnicator.RandomMethod
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

                    ::

                        RandomMethod ()


                    A random test method."""
                ),
                rst,
            )

    def test_generate_rst_signal(self):
        """Test generating a signal documentation with the rst generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <!-- RandomSignal:

            A random test signal.
            -->
            <signal name="RandomSignal"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-rst",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.rst", "r") as f:
            rst = f.read()
            self.assertIn(
                textwrap.dedent(
                    """
                    -------
                    Signals
                    -------

                    .. _org.project.Bar.Frobnicator::RandomSignal:

                    org.project.Bar.Frobnicator::RandomSignal
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

                    ::

                        RandomSignal ()


                    A random test signal."""
                ),
                rst,
            )

    def test_generate_rst_property(self):
        """Test generating a property documentation with the rst generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <!-- RandomProperty:

            A random test property.
            -->
            <property type="s" name="RandomProperty" access="read"/>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-rst",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.rst", "r") as f:
            rst = f.read()
            self.assertIn(
                textwrap.dedent(
                    """
                    ----------
                    Properties
                    ----------

                    .. _org.project.Bar.Frobnicator:RandomProperty:

                    org.project.Bar.Frobnicator:RandomProperty
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

                    ::

                        RandomProperty readable s


                    A random test property."""
                ),
                rst,
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_min_required_invalid(self):
        """Test running with an invalid --glib-min-required."""
        with self.assertRaises(subprocess.CalledProcessError):
            self.runCodegenWithInterface(
                "",
                "--output",
                "/dev/stdout",
                "--body",
                "--glib-min-required",
                "hello mum",
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_min_required_too_low(self):
        """Test running with a --glib-min-required which is too low (and hence
        probably a typo)."""
        with self.assertRaises(subprocess.CalledProcessError):
            self.runCodegenWithInterface(
                "", "--output", "/dev/stdout", "--body", "--glib-min-required", "2.6"
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_min_required_major_only(self):
        """Test running with a --glib-min-required which contains only a major version."""
        result = self.runCodegenWithInterface(
            "",
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-min-required",
            "3",
            "--glib-max-allowed",
            "3.2",
        )
        self.assertEqual("", result.err)
        self.assertNotEqual("", result.out.strip())

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_min_required_with_micro(self):
        """Test running with a --glib-min-required which contains a micro version."""
        result = self.runCodegenWithInterface(
            "", "--output", "/dev/stdout", "--header", "--glib-min-required", "2.46.2"
        )
        self.assertEqual("", result.err)
        self.assertNotEqual("", result.out.strip())

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_max_allowed_too_low(self):
        """Test running with a --glib-max-allowed which is too low (and hence
        probably a typo)."""
        with self.assertRaises(subprocess.CalledProcessError):
            self.runCodegenWithInterface(
                "", "--output", "/dev/stdout", "--body", "--glib-max-allowed", "2.6"
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_max_allowed_major_only(self):
        """Test running with a --glib-max-allowed which contains only a major version."""
        result = self.runCodegenWithInterface(
            "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "3"
        )
        self.assertEqual("", result.err)
        self.assertNotEqual("", result.out.strip())

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_max_allowed_with_micro(self):
        """Test running with a --glib-max-allowed which contains a micro version."""
        result = self.runCodegenWithInterface(
            "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "2.46.2"
        )
        self.assertEqual("", result.err)
        self.assertNotEqual("", result.out.strip())

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_max_allowed_unstable(self):
        """Test running with a --glib-max-allowed which is unstable. It should
        be rounded up to the next stable version number, and hence should not
        end up less than --glib-min-required."""
        result = self.runCodegenWithInterface(
            "",
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-max-allowed",
            "2.63",
            "--glib-min-required",
            "2.64",
        )
        self.assertEqual("", result.err)
        self.assertNotEqual("", result.out.strip())

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_glib_max_allowed_less_than_min_required(self):
        """Test running with a --glib-max-allowed which is less than
        --glib-min-required."""
        with self.assertRaises(subprocess.CalledProcessError):
            self.runCodegenWithInterface(
                "",
                "--output",
                "/dev/stdout",
                "--body",
                "--glib-max-allowed",
                "2.62",
                "--glib-min-required",
                "2.64",
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_dbus_types(self):
        bad_types = [
            "{vs}",  # Bad dictionary key type
            "(ss(s{{sv}s}))",  # Bad dictionary key types
            "{s",  # Unterminated dictionary
            "(s{sss})",  # Unterminated dictionary
            "z",  # Bad type
            "(ssms)",  # Bad type
            "(",  # Unterminated tuple
            "(((ss))",  # Unterminated tuple
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas",  # Too much recursion
            "(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((("
            "(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((s))"
            "))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))"
            "))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))",  # Too much recursion
            "{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{"
            "{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{sv}"
            "}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}"
            "}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}",  # Too much recursion
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(aaaaaa{sv})",  # Too much recursion
            "(ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
            "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
            "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
            "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssss)",  # Too long
        ]
        for t in bad_types:
            interface_xml = f"""
                <node>
                  <interface name="BadTypes">
                    <property type="{t}" name="BadPropertyType" access="read" />
                  </interface>
                </node>"""
            with self.assertRaises(subprocess.CalledProcessError):
                self.runCodegenWithInterface(
                    interface_xml, "--output", "/dev/stdout", "--body"
                )
        good_types = [
            "si{s{b(ybnqiuxtdh)}}{yv}{nv}{dv}",
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas",  # 128 Levels of recursion
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(aaaaaaaaaaaaaaaaa{sv})",  # 128 Levels of recursion
        ]
        for t in good_types:
            interface_xml = f"""
                <node>
                  <interface name="GoodTypes">
                    <property type="{t}" name="GoodPropertyType" access="read" />
                  </interface>
                </node>"""
            result = self.runCodegenWithInterface(
                interface_xml, "--output", "/dev/stdout", "--body"
            )
            self.assertEqual("", result.err)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_unix_fd_types_and_annotations(self):
        """Test an interface with `h` arguments, no annotation, and GLib < 2.64.

        See issue #1726.
        """
        interface_xml = """
            <node>
              <interface name="FDPassing">
                <method name="HelloFD">
                  <annotation name="org.gtk.GDBus.C.UnixFD" value="1"/>
                  <arg name="greeting" direction="in" type="s"/>
                  <arg name="response" direction="out" type="s"/>
                </method>
                <method name="NoAnnotation">
                  <arg name="greeting" direction="in" type="h"/>
                  <arg name="greeting_locale" direction="in" type="s"/>
                  <arg name="response" direction="out" type="h"/>
                  <arg name="response_locale" direction="out" type="s"/>
                </method>
                <method name="NoAnnotationNested">
                  <arg name="files" type="a{sh}" direction="in"/>
                </method>
              </interface>
            </node>"""

        # Try without specifying --glib-min-required.
        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--header"
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GUnixFDList"), 6)

        # Specify an old --glib-min-required.
        result = self.runCodegenWithInterface(
            interface_xml,
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-min-required",
            "2.32",
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GUnixFDList"), 6)

        # Specify a --glib-min-required ≥ 2.64. There should be more
        # mentions of `GUnixFDList` now, since the annotation is not needed to
        # trigger its use.
        result = self.runCodegenWithInterface(
            interface_xml,
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-min-required",
            "2.64",
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GUnixFDList"), 18)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_call_flags_and_timeout_method_args(self):
        """Test that generated method call functions have @call_flags and
        @timeout_msec args if and only if GLib >= 2.64.
        """
        interface_xml = """
            <node>
              <interface name="org.project.UsefulInterface">
                <method name="UsefulMethod"/>
              </interface>
            </node>"""

        # Try without specifying --glib-min-required.
        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--header"
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0)
        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0)

        # Specify an old --glib-min-required.
        result = self.runCodegenWithInterface(
            interface_xml,
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-min-required",
            "2.32",
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0)
        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0)

        # Specify a --glib-min-required ≥ 2.64. The two arguments should be
        # present for both the async and sync method call functions.
        result = self.runCodegenWithInterface(
            interface_xml,
            "--output",
            "/dev/stdout",
            "--header",
            "--glib-min-required",
            "2.64",
        )
        self.assertEqual("", result.err)
        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 2)
        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 2)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signal_id_simple_signal(self):
        """Test that signals IDs are used to emit signals"""
        interface_xml = """
            <node>
              <interface name="org.project.UsefulInterface">
                <signal name="SimpleSignal"/>
              </interface>
              <interface name="org.project.OtherIface">
                <signal name="SimpleSignal"/>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_signal_emit_by_name ("), 0)

        for iface in ["USEFUL_INTERFACE", "OTHER_IFACE"]:
            enum_name = f"_ORG_PROJECT_{iface}_SIGNALS"
            enum_item = f"_ORG_PROJECT_{iface}_SIMPLE_SIGNAL"
            self.assertIs(stripped_out.count(f"{enum_item},"), 1)
            self.assertIs(stripped_out.count(f"{enum_name}[{enum_item}] ="), 1)
            self.assertIs(
                stripped_out.count(
                    f" g_signal_emit (object, {enum_name}[{enum_item}], 0);"
                ),
                1,
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signal_id_multiple_signals_types(self):
        """Test that signals IDs are used to emit signals for all types"""

        signal_template = "<signal name='{}'><arg name='{}' type='{}'/></signal>"
        generated_signals = [
            signal_template.format(
                f"SingleArgSignal{t.upper()}", f"an_{t}", props.get("variant_type", t)
            )
            for t, props in self.ARGUMENTS_TYPES.items()
        ]

        interface_xml = f"""
            <node>
              <interface name="org.project.SignalingIface">
                <signal name="NoArgSignal" />
                {''.join(generated_signals)}
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_signal_emit_by_name ("), 0)

        iface = "SIGNALING_IFACE"
        for t in self.ARGUMENTS_TYPES.keys():
            enum_name = f"_ORG_PROJECT_{iface}_SIGNALS"
            enum_item = f"_ORG_PROJECT_{iface}_SINGLE_ARG_SIGNAL_{t.upper()}"
            self.assertIs(stripped_out.count(f"{enum_item},"), 1)
            self.assertIs(stripped_out.count(f"{enum_name}[{enum_item}] ="), 1)
            self.assertIs(
                stripped_out.count(
                    f" g_signal_emit (object, {enum_name}[{enum_item}], 0, arg_an_{t});"
                ),
                1,
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signal_id_multiple_signal_args_types(self):
        """Test that signals IDs are used to emit signals for all types"""

        generated_args = [
            f"<arg name='an_{t}' type='{props.get('variant_type', t)}'/>\n"
            for t, props in self.ARGUMENTS_TYPES.items()
        ]

        interface_xml = f"""
            <node>
              <interface name="org.project.SignalingIface">
                <signal name="SignalWithManyArgs">
                    {''.join(generated_args)}
                </signal>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_signal_emit_by_name ("), 0)

        iface = "SIGNALING_IFACE"
        enum_name = f"_ORG_PROJECT_{iface}_SIGNALS"
        enum_item = f"_ORG_PROJECT_{iface}_SIGNAL_WITH_MANY_ARGS"
        self.assertIs(stripped_out.count(f"{enum_item},"), 1)
        self.assertIs(stripped_out.count(f"{enum_name}[{enum_item}] ="), 1)

        args = ", ".join([f"arg_an_{t}" for t in self.ARGUMENTS_TYPES.keys()])
        self.assertIs(
            stripped_out.count(
                f" g_signal_emit (object, {enum_name}[{enum_item}], 0, {args});"
            ),
            1,
        )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signals_marshaller_simple_signal(self):
        """Test that signals marshaller is generated for simple signal"""
        interface_xml = """
            <node>
              <interface name="org.project.SignalingIface">
                <signal name="SimpleSignal"/>
              </interface>
              <interface name="org.project.OtherSignalingIface">
                <signal name="SimpleSignal"/>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = "org_project_signaling_iface_signal_marshal_simple_signal"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)
        self.assertIs(stripped_out.count("g_cclosure_marshal_VOID__VOID (closure"), 2)

        func_name = "org_project_other_signaling_iface_signal_marshal_simple_signal"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)
        self.assertIs(stripped_out.count("g_cclosure_marshal_VOID__VOID (closure"), 2)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signals_marshaller_single_typed_args(self):
        """Test that signals marshaller is generated for each known type"""
        for t, props in self.ARGUMENTS_TYPES.items():
            camel_type = t[0].upper() + t[1:]
            interface_xml = f"""
            <node>
            <interface name="org.project.SignalingIface">
                <signal name="SimpleSignal"/>
                <signal name="SingleArgSignal{camel_type}">
                    <arg name="arg_{t}" type="{props.get("variant_type", t)}"/>
                </signal>
            </interface>
            </node>"""

            result = self.runCodegenWithInterface(
                interface_xml, "--output", "/dev/stdout", "--body"
            )
            stripped_out = result.out.strip()
            self.assertFalse(result.err)
            self.assertEqual(stripped_out.count("g_cclosure_marshal_generic"), 0)

            self.assertIs(
                stripped_out.count("g_cclosure_marshal_VOID__VOID (closure"), 1
            )

            func_name = (
                f"org_project_signaling_iface_signal_marshal_single_arg_signal_{t}"
            )
            self.assertIs(stripped_out.count(f"{func_name},"), 1)
            self.assertIs(stripped_out.count(f"{func_name} ("), 1)

            if props.get("lacks_marshaller", False):
                self.assertIs(
                    stripped_out.count(
                        f"g_marshal_value_peek_{props['value_type']} (param_values + 1)"
                    ),
                    1,
                )
            else:
                self.assertIs(
                    stripped_out.count(
                        f"g_cclosure_marshal_VOID__{props['value_type'].upper()} (closure"
                    ),
                    1,
                )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_signals_marshallers_multiple_args(self):
        """Test that signals marshallers are generated"""
        generated_args = [
            f"<arg name='an_{t}' type='{props.get('variant_type', t)}'/>\n"
            for t, props in self.ARGUMENTS_TYPES.items()
        ]

        interface_xml = f"""
            <node>
              <interface name="org.project.SignalingIface">
                <signal name="SimpleSignal"/>
                <signal name="SignalWithManyArgs">
                    {''.join(generated_args)}
                </signal>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = f"org_project_signaling_iface_signal_marshal_simple_signal"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        func_name = f"org_project_signaling_iface_signal_marshal_signal_with_many_args"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        # Check access to MultipleArgsSignal arguments
        index = 1
        for props in self.ARGUMENTS_TYPES.values():
            self.assertIs(
                stripped_out.count(
                    f"g_marshal_value_peek_{props['value_type']} (param_values + {index})"
                ),
                1,
            )
            index += 1

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshaller_simple_method(self):
        """Test that methods marshaller is generated for simple method"""
        interface_xml = """
            <node>
              <interface name="org.project.CallableIface">
                <method name="SimpleMethod"/>
              </interface>
              <interface name="org.project.OtherCallableIface">
                <method name="SimpleMethod"/>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = "org_project_callable_iface_method_marshal_simple_method"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        func_name = "org_project_other_callable_iface_method_marshal_simple_method"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        self.assertIs(
            stripped_out.count("g_marshal_value_peek_object (param_values + 1)"), 1
        )
        self.assertIs(
            stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
        )

        self.assertIs(
            stripped_out.count(
                "_g_dbus_codegen_marshal_BOOLEAN__OBJECT (\n    GClosure"
            ),
            1,
        )
        self.assertIs(
            stripped_out.count("_g_dbus_codegen_marshal_BOOLEAN__OBJECT (closure"), 2
        )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshaller_single_typed_in_args(self):
        """Test that methods marshallers are generated for each known type"""
        for t, props in self.ARGUMENTS_TYPES.items():
            camel_type = t[0].upper() + t[1:]
            interface_xml = f"""
            <node>
            <interface name="org.project.UsefulInterface">
                <method name="SingleArgMethod{camel_type}">
                    <arg name="arg_{t}" type="{props.get("variant_type", t)}"/>
                </method>
            </interface>
            </node>"""

            result = self.runCodegenWithInterface(
                interface_xml, "--output", "/dev/stdout", "--body"
            )
            stripped_out = result.out.strip()
            self.assertFalse(result.err)
            self.assertEqual(stripped_out.count("g_cclosure_marshal_generic"), 0)

            func_name = (
                f"org_project_useful_interface_method_marshal_single_arg_method_{t}"
            )
            self.assertIs(stripped_out.count(f"{func_name},"), 1)
            self.assertIs(stripped_out.count(f"{func_name} ("), 1)
            self.assertIs(
                stripped_out.count("g_marshal_value_peek_object (param_values + 1)"), 1
            )
            self.assertIs(
                stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
            )
            self.assertIs(
                stripped_out.count(
                    f"g_marshal_value_peek_{props['value_type']} (param_values + 2)"
                ),
                1,
            )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshaller_single_typed_out_args(self):
        """Test that methods marshallers are generated for each known type"""
        for t, props in self.ARGUMENTS_TYPES.items():
            camel_type = t[0].upper() + t[1:]
            interface_xml = f"""
            <node>
            <interface name="org.project.UsefulInterface">
                <method name="SingleArgMethod{camel_type}">
                    <arg name="arg_{t}" type="{props.get("variant_type", t)}" direction="out"/>
                </method>
            </interface>
            </node>"""

            result = self.runCodegenWithInterface(
                interface_xml, "--output", "/dev/stdout", "--body"
            )
            stripped_out = result.out.strip()
            self.assertFalse(result.err)
            self.assertEqual(stripped_out.count("g_cclosure_marshal_generic"), 0)

            func_name = (
                f"org_project_useful_interface_method_marshal_single_arg_method_{t}"
            )
            self.assertIs(stripped_out.count(f"{func_name},"), 1)
            self.assertIs(stripped_out.count(f"{func_name} ("), 1)
            self.assertIs(
                stripped_out.count("g_marshal_value_peek_object (param_values + 1)"), 1
            )
            self.assertIs(
                stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
            )
            self.assertIs(stripped_out.count("(param_values + 2)"), 0)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshallers_multiple_in_args(self):
        """Test that methods marshallers are generated"""
        generated_args = [
            f"<arg name='an_{t}' type='{props.get('variant_type', t)}'/>\n"
            for t, props in self.ARGUMENTS_TYPES.items()
        ]

        interface_xml = f"""
            <node>
              <interface name="org.project.CallableIface">
                <method name="MethodWithManyArgs">
                    {''.join(generated_args)}
                </method>
                <method name="SameMethodWithManyArgs">
                    {''.join(generated_args)}
                </method>
              </interface>
              <interface name="org.project.OtherCallableIface">
                <method name="MethodWithManyArgs">
                    {''.join(generated_args)}
                </method>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = f"org_project_callable_iface_method_marshal_method_with_many_args"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        # Check access to MultipleArgsMethod arguments
        index = 1
        self.assertIs(
            stripped_out.count(f"g_marshal_value_peek_object (param_values + {index})"),
            1,
        )
        index += 1

        for props in self.ARGUMENTS_TYPES.values():
            self.assertIs(
                stripped_out.count(
                    f"g_marshal_value_peek_{props['value_type']} (param_values + {index})"
                ),
                1,
            )
            index += 1

        self.assertIs(
            stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
        )
        func_types = "_".join(
            [p["value_type"].upper() for p in self.ARGUMENTS_TYPES.values()]
        )
        func_name = f"_g_dbus_codegen_marshal_BOOLEAN__OBJECT_{func_types}"
        self.assertIs(stripped_out.count(f"{func_name} (\n    GClosure"), 1)
        self.assertIs(stripped_out.count(f"{func_name} (closure"), 3)

        func_name = (
            f"org_project_other_callable_iface_method_marshal_method_with_many_args"
        )
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshallers_multiple_out_args(self):
        """Test that methods marshallers are generated"""
        generated_args = [
            f"<arg name='an_{t}' type='{props.get('variant_type', t)}' direction='out'/>\n"
            for t, props in self.ARGUMENTS_TYPES.items()
        ]

        interface_xml = f"""
            <node>
              <interface name="org.project.CallableIface">
                <method name="MethodWithManyArgs">
                    {''.join(generated_args)}
                </method>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = f"org_project_callable_iface_method_marshal_method_with_many_args"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        # Check access to MultipleArgsMethod arguments
        index = 1
        self.assertIs(
            stripped_out.count(f"g_marshal_value_peek_object (param_values + {index})"),
            1,
        )
        index += 1

        for index in range(index, len(self.ARGUMENTS_TYPES)):
            self.assertIs(stripped_out.count(f"(param_values + {index})"), 0)

        self.assertIs(
            stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
        )

        self.assertIs(
            stripped_out.count("_g_dbus_codegen_marshal_BOOLEAN__OBJECT (closure"),
            1,
        )

    @unittest.skipIf(on_win32(), "requires /dev/stdout")
    def test_generate_methods_marshallers_with_unix_fds(self):
        """Test an interface with `h` arguments"""
        interface_xml = """
            <node>
              <interface name="test.FDPassing">
                <method name="HelloFD">
                  <annotation name="org.gtk.GDBus.C.UnixFD" value="1"/>
                  <arg name="greeting" direction="in" type="s"/>
                  <arg name="response" direction="out" type="s"/>
                </method>
              </interface>
            </node>"""

        result = self.runCodegenWithInterface(
            interface_xml, "--output", "/dev/stdout", "--body"
        )
        stripped_out = result.out.strip()
        self.assertFalse(result.err)
        self.assertIs(stripped_out.count("g_cclosure_marshal_generic"), 0)

        func_name = f"test_fdpassing_method_marshal_hello_fd"
        self.assertIs(stripped_out.count(f"{func_name},"), 1)
        self.assertIs(stripped_out.count(f"{func_name} ("), 1)

        index = 1
        self.assertIs(
            stripped_out.count(f"g_marshal_value_peek_object (param_values + {index})"),
            1,
        )
        index += 1

        self.assertIs(
            stripped_out.count(f"g_marshal_value_peek_object (param_values + {index})"),
            1,
        )

        index += 1
        self.assertIs(
            stripped_out.count(f"g_marshal_value_peek_string (param_values + {index})"),
            1,
        )
        index += 1

        self.assertIs(
            stripped_out.count("g_value_set_boolean (return_value, v_return);"), 1
        )

    def test_generate_valid_docbook(self):
        """Test the basic functionality of the docbook generator."""
        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <!-- Resize:
                 @size: New partition size in bytes, 0 for maximal size.
                 @options: Options.
                 @since 2.7.2

                 Resizes the partition.

                 The partition will not change its position but might be slightly bigger
                 than requested due to sector counts and alignment (e.g. 1MiB).
                 If the requested size can't be allocated it results in an error.
                 The maximal size can automatically be set by using 0 as size.
            -->
            <method name="Resize">
              <arg name="size" direction="in" type="t"/>
              <arg name="options" direction="in" type="a{sv}"/>
            </method>
          </interface>
        </node>
        """
        res = self.runCodegenWithInterface(
            xml_contents,
            "--generate-docbook",
            "test",
        )
        self.assertEqual("", res.err)
        self.assertEqual("", res.out)
        with open("test-org.project.Bar.Frobnicator.xml", "r") as f:
            self.assertTrue(ET.parse(f) is not None)

    def test_indentation_preservation_in_comments(self):
        """Test if the parser preserves relative indentation in XML comments"""
        markup_list = """\
- The mnemonic key activates the object if it is presently enabled onscreen.
  This typically corresponds to the underlined letter within the widget.
  Example: "n" in a traditional "New..." menu item or the "a" in "Apply" for
  a button."""

        xml_contents = """
        <node>
          <interface name="org.project.Bar.Frobnicator">
            <!-- GetKeyBinding:
                 @index: 0-based index of the action to query.

                 Gets the keybinding which can be used to activate this action, if one
                 exists. The string returned should contain localized, human-readable,
                 key sequences as they would appear when displayed on screen. It must
                 be in the format "mnemonic;sequence;shortcut".

                 - The mnemonic key activates the object if it is presently enabled onscreen.
                   This typically corresponds to the underlined letter within the widget.
                   Example: "n" in a traditional "New..." menu item or the "a" in "Apply" for
                   a button.

                 If there is no key binding for this action, return "".
            -->
            <method name="GetKeyBinding">
              <arg type="i" name="index" direction="in"/>
              <arg type="s" direction="out"/>
            </method>
          </interface>
        </node>
        """
        for format, ext in [("rst", "rst"), ("md", "md"), ("docbook", "xml")]:
            res = self.runCodegenWithInterface(
                xml_contents, f"--generate-{format}", "test"
            )
            self.assertFalse(res.err)
            self.assertFalse(res.out)
            with open(f"test-org.project.Bar.Frobnicator.{ext}", "r") as f:
                self.assertIn(markup_list, f.read())


if __name__ == "__main__":
    unittest.main(testRunner=taptestrunner.TAPTestRunner())
